package com.jacky.utils {
	
	import flash.display.ActionScriptVersion;
	import flash.errors.EOFError;
	import flash.geom.*;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	
	/**
	 * Reads the bytes of a SWF (as a ByteArray) to acquire 
	 * information from the SWF file header.  Some of
	 * this information is inaccessible to ActionScript
	 * otherwise.
	 */
	public class SWFReader {

		// properties starting with those
		// found first in the byte stream
		
		/**
		 * Indicates whether or not the SWF
		 * is compressed.
		 */
		public var compressed:Boolean;
		
		/**
		 * The major version of the SWF.
		 */
		public var version:uint;
		
		/**
		 * The file size of the SWF.
		 */
		public var fileSize:uint;
		
		// compression starts here if SWF compressed:
		
		/**
		 * The dimensions of the SWF in the form of
		 * a Rectangle instance. 
		 */
		public function get dimensions():Rectangle { return _dimensions; }
		private var _dimensions:Rectangle = new Rectangle();
		// dimensions gets accessor since we don't
		// want people nulling the rect; it should
		// always have a value, even if 0-ed
		
		/**
		 * Width of the stage as defined by the SWF.
		 * Same as dimensions.width.
		 */
		public function get width():uint {
			return uint(_dimensions.width);
		}
		
		/**
		 * Height of the stage as defined by the SWF.
		 * Same as dimensions.height.
		 */
		public function get height():uint {
			return uint(_dimensions.height);
		}
		
		/**
		 * When true, the bytes supplied in calls made to the tagCallback
		 * function includes the RECORDHEADER of that tag which includes
		 * the tag and the size of the tag. By default (false) this
		 * information is not included.
		 */
		public function get tagCallbackBytesIncludesHeader():Boolean {
			return _tagCallbackBytesIncludesHeader;
		}
		public function set tagCallbackBytesIncludesHeader(value:Boolean):void {
			_tagCallbackBytesIncludesHeader = value;
		}
		private var _tagCallbackBytesIncludesHeader:Boolean = false;
		
		/**
		 * The frame rate of the SWF in frames
		 * per second.
		 */
		public var frameRate:uint;
		
		/**
		 * The total number of frames of the SWF.
		 */
		public var totalFrames:uint;
		
		/**
		 * ActionScript version.
		 */
		public var asVersion:uint;
		
		/**
		 * Determines local playback security; when
		 * true, indicates that when this file is
		 * run locally, it can only access the network.
		 * When false, only local files can be accessed.
		 * This does not apply when the SWF is being
		 * run in a local-trusted sandbox.
		 */
		public var usesNetwork:Boolean;
		
		/**
		 * The background color of the SWF.
		 */
		public var backgroundColor:uint;
		
		/**
		 * Determines if the SWF is protected from
		 * being imported into an authoring tool.
		 */
		public var protectedFromImport:Boolean;
		
		/**
		 * Determines if remote debugging is enabled.
		 */
		public var debuggerEnabled:Boolean;
		
		/**
		 * The XMP metadata defined in the SWF.
		 */
		public var metadata:XML;
		
		/**
		 * Maximun allowed levels of recursion.
		 */
		public var recursionLimit:uint;
		
		/**
		 * Time in seconds a script will run in a
		 * single frame before a timeout error
		 * occurs.
		 */
		public var scriptTimeoutLimit:uint;
		
		/**
		 * The level of hardware acceleration specified
		 * for the SWF. 0 is none, 1 is direct, and 2
		 * is GPU (Flash Player 10+).
		 */
		public var hardwareAcceleration:uint;
		
		/**
		 * A callback function that will be called when
		 * a tag is read during the parse process. The
		 * callback function should contain the parameters
		 * (tag:uint, bytes:ByteArray).
		 */
		public var tagCallback:Function;
		
		/**
		 * Indicates that the SWF bytes last provided
		 * were successfully parsed. If the SWF bytes
		 * were not successfully parsed, no SWF data
		 * will be available.
		 */
		public var parsed:Boolean;
		
		/**
		 * The Flash Player error message that resulted
		 * from the error that caused a parse to fail.
		 */
		public var errorText:String = "";
		
		// keeping track of data
		private var bytes:ByteArray;
		private var currentByte:int; // used in bit reading
		private var bitPosition:int; // used in bit reading
		private var currentTag:uint;
		
		// tag flags
		private var bgColorFound:Boolean;
		
		// constants
		private const GET_DATA_SIZE:int			= 5;
		private const TWIPS_TO_PIXELS:Number	= 0.05; // 20 twips in a pixel
		private const TAG_HEADER_ID_BITS:int	= 6;
		private const TAG_HEADER_MAX_SHORT:int	= 0x3F;
		
		private const SWF_C:uint				= 0x43; // header characters
		private const SWF_F:uint				= 0x46;
		private const SWF_W:uint				= 0x57;
		private const SWF_S:uint				= 0x53;
		
		private const TAG_ID_EOF:uint			= 0; // recognized SWF tags
		private const TAG_ID_BG_COLOR:uint		= 9;
		private const TAG_ID_PROTECTED:uint		= 24;
		private const TAG_ID_DEBUGGER1:uint		= 58;
		private const TAG_ID_DEBUGGER2:uint		= 64;
		private const TAG_ID_SCRIPT_LIMITS:uint	= 65;
		private const TAG_ID_FILE_ATTS:uint		= 69;
		private const TAG_ID_META:uint			= 77;
		
		private const TAG_ID_SHAPE_1:uint		= 2;
		private const TAG_ID_SHAPE_2:uint		= 22;
		private const TAG_ID_SHAPE_3:uint		= 32;
		private const TAG_ID_SHAPE_4:uint		= 83;
		
		/**
		 * SWFHeader constructor.
		 * @param	swfBytes Bytes of the SWF in a ByteArray.
		 * You can get the bytes of a SWF by loading it into
		 * a URLLoader or using Loader.bytes once a SWF has
		 * been loaded into that Loader.
		 */
		public function SWFReader(swfBytes:ByteArray = null) {
			parse(swfBytes);
		}
		
		/**
		 * Provides a string presentation of the SWFHeader
		 * object which outlines the different values
		 * obtained from a parsed SWF
		 * @return The String form of the instance
		 */
		public function toString():String {
			if (parsed) {
				var compression:String	= (compressed) ? "compressed" : "uncompressed";
				var frames:String		= totalFrames > 1 ? "frames" : "frame";
				return "[SWF" + version + " AS"+asVersion+".0: " + totalFrames + " "+frames+" @ " + frameRate + " fps "
					+ _dimensions.width + "x" + _dimensions.height + " " + compression + "]";
			}
			
			// default toString if SWF not parsed
			return Object.prototype.toString.call(this);
		}
		
		/**
		 * Parses the bytes of a SWF file to extract
		 * properties from its header.
		 * @param	swfBytes Bytes of a SWF to parse.
		 */
		public function parse(swfBytes:ByteArray):void {
			parseDefaults();
			
			// null bytes, exit
			if (swfBytes == null) {
				parseError("Error: Cannot parse a null value.");
				return;
			}
			
				
			// assume at start parse completed successfully
			// on failure, this will be set to false
			parsed = true;
			
			// --------------------------------------
			// HEADER
			// --------------------------------------
			
			try {
				
				// try to parse the bytes.  Failures
				// results in cleared values for the data
				bytes			= swfBytes;
				bytes.endian	= Endian.LITTLE_ENDIAN;
				bytes.position	= 0;
				
				// get header characters
				var swfFC:uint	= bytes.readUnsignedByte(); // F, or C if compressed
				var swfW:uint 	= bytes.readUnsignedByte(); // W
				var swfS:uint	= bytes.readUnsignedByte(); // S
				
				// validate header characters
				if ((swfFC != SWF_F && swfFC != SWF_C)
				||   swfW  != SWF_W || swfS  != SWF_S) {
					parseError("Error: Invalid SWF header.");
					return;
				}
				
				compressed	= Boolean(swfFC == SWF_C); // == SWF_F if not compressed
				
				version		= bytes.readUnsignedByte();
				
				fileSize	= bytes.readUnsignedInt(); // mostly redundant since we should have full bytes
				
				// if compressed, need to uncompress
				// the data after the first 8 bytes
				// (first 8 already read above)
				if (compressed) {
					
					// use a temporary byte array to
					// represent the compressed portion
					// of the SWF file 
					var temp:ByteArray = new ByteArray();
					bytes.readBytes(temp);
					bytes			= temp;
					bytes.endian	= Endian.LITTLE_ENDIAN;
					bytes.position	= 0;
					temp			= null; // temp no longer needed
					
					bytes.uncompress();
					
					// Note: at this point, the original
					// uncompressed 8 bytes are no longer
					// part of the current bytes byte array
				}
				
				_dimensions	= readRect();
				bytes.position++; // one up after rect
				
				frameRate	= bytes.readUnsignedByte();
				
				totalFrames	= bytes.readUnsignedShort();
				
			}catch (error:Error) {
				
				// header parse error
				parseError(error.message);
				return;
			}
			
			// --------------------------------------
			// TAGS
			// --------------------------------------
			
			// read all the tags in the file
			// up until the END tag
			try {
				while(readTag());
			}catch (error:Error) {
				
				// error in tag parsing. EOF would throw
				// an error, but the END tag should be
				// reached before that occurs
				parseError(error.message);
				return;
			}
			
			// parse completed successfully!
			// null bytes since no longer needed
			bytes = null;
		}
		
		/**
		 * Defines default values for all the class
		 * properties.  This is used to have accurate
		 * values for properties which may not be 
		 * present in the SWF file such as asVersion
		 * which is only required to be specified in
		 * SWF8 and above (in FileAttributes tag).
		 */
		private function parseDefaults():void {
			compressed		= false;
			version			= 1; // SWF1
			fileSize		= 0;
			_dimensions		= new Rectangle();
			frameRate		= 12; // default from Flash authoring (flex == 24)
			totalFrames		= 1;
			metadata		= null;
			asVersion		= ActionScriptVersion.ACTIONSCRIPT2; // 2 if not explicit
			usesNetwork		= false;
			backgroundColor	= 0xFFFFFF; // white background
			protectedFromImport	= false;
			debuggerEnabled	= true;
			scriptTimeoutLimit	= 256;
			recursionLimit	= 15;
			hardwareAcceleration	= 0;
			
			errorText		= ""; // clear existing error text
			
			// tag helper flags
			bgColorFound	= false;
		}
		
		/**
		 * Clears variable data and logs an error
		 * message.
		 */
		private function parseError(message:String = "Unkown error."):void {
			compressed		= false;
			version			= 0;
			fileSize		= 0;
			_dimensions		= new Rectangle();
			frameRate		= 0;
			totalFrames		= 0;
			metadata		= null;
			asVersion		= 0;
			usesNetwork		= false;
			backgroundColor	= 0;
			protectedFromImport	= false;
			debuggerEnabled	= false;
			scriptTimeoutLimit	= 0;
			recursionLimit	= 0;
			hardwareAcceleration	= 0;
			
			parsed			= false;
			bytes			= null;
			errorText		= message;
		}
		
		/**
		 * Utility to convert a unit value into a string
		 * in hex style padding value with "0" characters.
		 * @return The string representation of the hex value.
		 */
		private function paddedHex(value:uint, numChars:int = 6):String {
			var str:String = value.toString(16);
			while (str.length < numChars) str = "0" + str;
			return "0x" + str;
		}
		
		/**
		 * Reads a string in the byte stream by
		 * reading all bytes until a null byte (0)
		 * is reached.
		 * @return The string having been read.
		 */
		private function readString():String {
			
			// find ending null character that
			// terminates the string
			var i:uint = bytes.position;
			try {
				while(bytes[i] != 0) i++;
			}catch (error:Error) {
				return "";
			}
			
			// null byte should have been found
			// return the read string
			return bytes.readUTFBytes(i - bytes.position);
		}

		/**
		 * Reads RECT data from the current
		 * location in the current bytes object
		 * @return A rectangle object whose values
		 * match those of the RECT read.
		 */
		private function readRect():Rectangle {
			nextBitByte();
			var rect:Rectangle	= new Rectangle();
			var dataSize:uint	= readBits(GET_DATA_SIZE);
			rect.left			= readBits(dataSize, true)*TWIPS_TO_PIXELS;
			rect.right			= readBits(dataSize, true)*TWIPS_TO_PIXELS;
			rect.top			= readBits(dataSize, true)*TWIPS_TO_PIXELS;
			rect.bottom			= readBits(dataSize, true)*TWIPS_TO_PIXELS;
			return rect;
		}
		
		private function readMatrix():Matrix {
			nextBitByte();
			var dataSize:uint;
			var matrix:Matrix = new Matrix();
			
			if (readBits(1)){ // has scale
				dataSize	= readBits(GET_DATA_SIZE);
				matrix.a	= readBits(dataSize, true);
				matrix.d	= readBits(dataSize, true);
			}
			
			if (readBits(1)){ // has rotation
				dataSize	= readBits(GET_DATA_SIZE);
				matrix.b	= readBits(dataSize, true);
				matrix.c	= readBits(dataSize, true);
			}
			
			// translation
			dataSize	= readBits(GET_DATA_SIZE);
			matrix.tx	= readBits(dataSize, true)*TWIPS_TO_PIXELS;
			matrix.ty	= readBits(dataSize, true)*TWIPS_TO_PIXELS;
			
			return matrix;
		}
		
		/**
		 * Reads a series of bits from the current byte
		 * defined by currentByte based on the but at
		 * position bitPosition.  If more bits are required
		 * than are available in the current byte, the next
		 * byte in the bytes array is read and the bits are
		 * taken from there to complete the request.
		 * @param	numBits The number of bits to read.
		 * @return The bits read as a uint.
		 */
		private function readBits(numBits:uint, signed:Boolean = false):Number {
			var value:Number	= 0; // int or uint
			var remaining:uint	= 8 - bitPosition;
			var mask:uint;
			
			// can get all bits from current byte
			if (numBits <= remaining){
				mask	= (1 << numBits) - 1;
				value	= (currentByte >> (remaining - numBits)) & mask;
				if (numBits == remaining) nextBitByte();
				else bitPosition += numBits;
			
			// have to get bits from 2 (or more)
			// bytes the current and the next (recursive)
			}else{
				mask				= (1 << remaining) - 1;
				var firstValue:uint	= currentByte & mask;
				var over:uint		= numBits - remaining;
				nextBitByte();
				value = (firstValue << over) | readBits(over);
			}
			
			// convert to signed int if signed bitflag exists
			if (signed && value >> (numBits - 1) == 1){
				remaining = 32 - numBits; // 32-bit uint
				mask = (1 << remaining) - 1;
				return int(mask << numBits | value);
			}
			
			// unsigned int
			return uint(value); 
		}
		
		/**
		 * Reads the next byte in the stream assigning
		 * it to currentByte and resets the value of
		 * bitPosition to 0.
		 */
		private function nextBitByte():void {
			currentByte = bytes.readByte();
			bitPosition = 0;
		}
		
		/**
		 * Parses the tag at the current byte location.
		 * @return false if the tag read is the END tag;
		 * true if more tags should be present in the file.
		 */
		private function readTag():Boolean {
			
			var currentTagPosition:uint = bytes.position;
			
			// read tag header
			var tagHeader:int = bytes.readUnsignedShort();
			currentTag = tagHeader >> TAG_HEADER_ID_BITS;
			var tagLength:uint = tagHeader & TAG_HEADER_MAX_SHORT;
			
			// if a long tag, the tag length will be
			// set to its maximum. If so, set
			// the tag length to the long length
			if (tagLength == TAG_HEADER_MAX_SHORT) {
				tagLength = bytes.readUnsignedInt();
			}
			
			// when the tag is read, the position
			// of the byte stream must be set to the
			// end of this tag for the start of the next
			// tag.  This assures the correct position
			// no matter what happens in readTagData()
			var nextTagPosition:uint = bytes.position + tagLength;
			
			// read the data in the tag (if supported)
			var moreTags:Boolean = readTagData(tagLength, currentTagPosition, nextTagPosition);
			if (!moreTags) return false; // end tag
			
			// next tag
			bytes.position = nextTagPosition;
			return true;
		}
		
		/**
		 * Called from readTag, this parses the value of individual
		 * tag based on the tag id read in the tag header.
		 * @param	tag A tag object containing a tag's id and length.
		 * @param	start The start position of the full tag.
		 * @param	end The end position of the full tag.
		 * @return false if the tag read is the END tag;
		 * true if more tags should be present in the file.
		 */
		private function readTagData(tagLength:uint, start:uint, end:uint):Boolean {
			
			// if defined, call the tag callback with
			// the tag id and a copy of the bytes 
			// specific to the tag
			if (tagCallback != null) {
				var tagBytes:ByteArray = new ByteArray();
				if (_tagCallbackBytesIncludesHeader){
					tagBytes.writeBytes(bytes, start, end - start);
				}else{
					if (tagLength){
						tagBytes.writeBytes(bytes, bytes.position, tagLength);
					}
				}
				tagBytes.position = 0;
				tagCallback(currentTag, tagBytes);
			}
			
			// handle each tag individually based on
			// it's tag id
			switch (currentTag) {
				
				
				// FileAttributes tag was only required for
				// SWF 8 and later. Calling defaults()
				// assures default values for the properties
				// determined here if the tag is not present
				case TAG_ID_FILE_ATTS:
				
					nextBitByte(); // read file attributes in bits
					readBits(1); // reserved
					
					hardwareAcceleration = readBits(2);
					
					readBits(1); // hasMetaData; auto-determined by tag
					
					asVersion = (readBits(1) && version >= 9)
						? ActionScriptVersion.ACTIONSCRIPT3
						: ActionScriptVersion.ACTIONSCRIPT2;
						
					readBits(2); // reserved (2)
					
					usesNetwork = Boolean(readBits(1) == 1);
					// bunch of others reserved after this
					break;
					
					
				// Metadata in a SWF is in the format of
				// XMP XML. Though the FileAttributes will
				// determine if it is present, it's easier to
				// just check for the metadata tag id
				case TAG_ID_META:
				
					try {
						metadata = new XML(readString());
					}catch (error:Error) {
						// error reading string or parsing as XML
					}
					break;
				
					
				// Many background colors could potentially exist
				// for a single SWF, but we're assuming there's
				// only one. If there are more, the first will be used
				case TAG_ID_BG_COLOR:
				
					// check the bg color found flag
					// if true, we want to ignore all other colors
					// since they would be added after this one
					if (!bgColorFound) {
						bgColorFound = true;
						
						backgroundColor = readRGB();
					}
					break;
				
					
				// Only determines if the SWF is protected from 
				// import; a password, if provided, will not
				// be retrieved from the SWF
				case TAG_ID_PROTECTED:
				
					protectedFromImport = Boolean(bytes.readUnsignedByte() != 0);
					// password if needed
					break;
					
					
				// the debugger 1 tag is for SWF5 only
				// the debugger 2 tag is for SWF6+
				case TAG_ID_DEBUGGER1:
				
					if (version == 5) debuggerEnabled = true;
					// password if needed
					break;
				case TAG_ID_DEBUGGER2:
				
					if (version > 5) debuggerEnabled = true;
					// password if needed
					break;
					
				// for both timeout and recursion but I don't
				// think any tool lets you set recursion
				case TAG_ID_SCRIPT_LIMITS:
					recursionLimit		= bytes.readUnsignedShort();
					scriptTimeoutLimit	= bytes.readUnsignedShort();
					break;
					
				case TAG_ID_EOF:
					return false; // end of file
					break;
					
				default:
					// unrecognized tag by this parser; do nothing
					// if you want to support other tags
					// make sure they're caught above in 
					// this switch statement.
					break;
			}
			
			// not last tag, continue reading
			return true;
		}
		private function readRGB():uint {
			return (bytes.readUnsignedByte() << 16) // R
				 | (bytes.readUnsignedByte() << 8)  // G
				 |  bytes.readUnsignedByte(); //       B
		}
		private function readARGB():uint {
			return (bytes.readUnsignedByte() << 24) // A
				 | (bytes.readUnsignedByte() << 16) // R
				 | (bytes.readUnsignedByte() << 8)  // G
				 |  bytes.readUnsignedByte(); //       B
		}
		private function readRGBA():uint {
			var rByte:uint = bytes.readUnsignedByte(); // R
			var gByte:uint = bytes.readUnsignedByte(); // G
			var bByte:uint = bytes.readUnsignedByte(); // B
			var aByte:uint = bytes.readUnsignedByte(); // A
			return (aByte << 24) // A
				 | (rByte << 16) // R
				 | (gByte << 8)  // G
				 |  bByte; //       B
		}
	}
}